

package com.googlecode.flac4j.format.metadata;


import com.googlecode.flac4j.format.FormatHelper;

import com.googlecode.jinahya.io.BitInput;
import com.googlecode.jinahya.io.BitOutput;

import java.io.IOException;
import java.io.UnsupportedEncodingException;

import java.util.Arrays;
import java.util.Collections;
import java.util.SortedMap;
import java.util.TreeMap;

import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlType;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;


/**
 *
 * @author Jin Kwon <jinahya at gmail.com>
 */
@XmlType(propOrder = {"catalogNumber", "sampleCount", "compactDisc", "tracks"})
public class Cuesheet extends AbstractData {


    public static final int MINIMUM_CATALOG_NUMBER_CHARACTER = 0x20;


    public static final int MAXIMUM_CATALOG_NUMBER_CHARACTER = 0x7E;


    public static final int CATALOG_NUMBER_LENGTH = 128;


    public Cuesheet() {
        super(BlockType.CUESHEET);
    }


    @Override
    public int getLengthInBytes() {

        int length = 396;

        for (Track track : tracks.values()) {
            length += track.getLengthInBytes();
        }

        return length;
    }


    @Override
    public void read(final BitInput input) throws IOException {

        catalogNumber = new byte[CATALOG_NUMBER_LENGTH];
        FormatHelper.readBytes(input, catalogNumber);

        setSampleCount(input.readLong(64));

        setCompactDisc(input.readBoolean());

        input.readUnsignedInt(7);
        for (int i = 0; i < 258; i++) {
            input.readUnsignedInt(8);;
        }

        final int trackCount = input.readUnsignedInt(8);
        tracks.clear();
        for (int i = 0; i < trackCount; i++) {
            final Track track = new Track();
            track.read(input);
            addTrack(track);
        }
    }


    @Override
    public void write(final BitOutput output) throws IOException {

        FormatHelper.writeBytes(output, getCatalogNumber());

        output.writeLong(64, getSampleCount());

        output.writeBoolean(isCompactDisc());

        output.writeUnsignedInt(7, 0x00);
        for (int i = 0; i < 258; i++) {
            output.writeUnsignedInt(8, 0x00);
        }

        for (Track track : tracks.values()) {
            track.write(output);
        }
    }


    // ----------------------------------------------------------- catalogNumber
    /**
     * Returns catalogNumber.
     *
     * @return catalogNumber.
     */
    public byte[] getCatalogNumber() {
        return Arrays.copyOf(catalogNumber, catalogNumber.length);
    }


    /**
     * Sets catalogNumber.
     *
     * @param catalogNumber catalogNumber
     */
    public void setCatalogNumber(final byte[] catalogNumber) {

        if (catalogNumber == null) {
            throw new NullPointerException("null catalogNumber");
        }

        if (catalogNumber.length > CATALOG_NUMBER_LENGTH) {
            throw new IllegalArgumentException(
                "catalogNumber.length(" + catalogNumber.length + ") > "
                + CATALOG_NUMBER_LENGTH);
        }

        this.catalogNumber =
            Arrays.copyOf(catalogNumber, CATALOG_NUMBER_LENGTH);
    }


    /**
     * Returns an US-ASCII String representation of catalogNumber. Any trailing
     * null characters will be trimmed.
     *
     * @return catalogNumber as a String.
     */
    public String getCatalogNumberAsString() {
        try {
            String string = new String(catalogNumber, "US-ASCII");
            final int index = string.indexOf('\0');
            if (index != -1) {
                string = string.substring(0, index);
            }
            return string;
        } catch (UnsupportedEncodingException uee) {
            throw new RuntimeException(uee);
        }
    }


    /**
     * Sets catalogNumber with an US-ASCII String. The specified String doesn't
     * necessarily have to contain any tailing null characters.
     *
     * @param string catalogNumber String
     */
    public void setCatalogNumberAsString(String string) {

        if (string == null) {
            throw new NullPointerException("null string");
        }

        final int index = string.indexOf('\0');
        if (index != -1) {
            string = string.substring(0, index);
        }

        try {
            setCatalogNumber(string.getBytes("US-ASCII"));
        } catch (UnsupportedEncodingException uee) {
            throw new RuntimeException(uee);
        }
    }


    // ------------------------------------------------------------- sampleCount
    /**
     * Returns sampleCount.
     *
     * @return sampleCount
     */
    public long getSampleCount() {
        return sampleCount;
    }


    /**
     * Sets sampleCount.
     *
     * @param sampleCount sampleCount
     */
    public void setSampleCount(final long sampleCount) {

        if (sampleCount < 0L) {
            throw new NullPointerException(
                "sampleCount(" + sampleCount + ") < 0L");
        }

        this.sampleCount = sampleCount;
    }


    // ------------------------------------------------------------- compactDisc
    /**
     * Returns compactDisc.
     *
     * @return compactDisc
     */
    public boolean isCompactDisc() {
        return compactDisc;
    }


    /**
     * Sets compactDisc.
     *
     * @param compactDisc compactDisc
     */
    public void setCompactDisc(final boolean compactDisc) {
        this.compactDisc = compactDisc;
    }


    // ------------------------------------------------------------------ tracks
    public void clearTracks() {
        tracks.clear();
    }


    public int getTrackCount() {
        return tracks.size();
    }


    public boolean isTracksEmpty() {
        return tracks.isEmpty();
    }


    /**
     * Returns next desired track number.
     *
     * @return next desired track number.
     */
    public int nextTrackNumber() {

        if (tracks.isEmpty()) {
            return 1;
        }

        return tracks.lastKey() + 1;
    }


    public void addTrack(final Track track) {

        if (track == null) {
            throw new NullPointerException("null track");
        }

        if (tracks.containsKey(track.getNumber())) {
            throw new IllegalArgumentException(
                "duplicated track.number: " + track.getNumber());
        }

        track.setCuesheet(this);

        tracks.put(track.getNumber(), track);
    }


    /**
     * Returns an unmodifiable map of track numbers and tracks.
     *
     * @return tracks
     */
    public SortedMap<Integer, Track> getTracks() {
        return Collections.unmodifiableSortedMap(tracks);
    }


    @XmlElement(required = true)
    private byte[] catalogNumber;


    @XmlElement(required = true)
    private long sampleCount;


    @XmlElement(required = true)
    private boolean compactDisc;


    @XmlElement(name = "track", required = true)
    //@XmlElementWrapper(required = true)
    @XmlJavaTypeAdapter(TracksAdapter.class)
    private final SortedMap<Integer, Track> tracks =
        new TreeMap<Integer, Track>();


}

